﻿/*
Copyright 2010 Matías Fidemraizer (www.matiasfidemraizer.com).

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using Comkarl.Architecture.Patterns.DomainModel;

namespace Comkarl.Architecture.Patterns.UnitOfWork
{
    /// <summary>
    /// Represents the base clase for an Unit of Work pattern implementation (<see cref="http://martinfowler.com/eaaCatalog/unitOfWork.html"/>).
    /// </summary>
    /// <typeparam name="TDomainObjectIdentifierName">
    /// The type for associated domain objects' identifier.
    /// </typeparam>
    /// <typeparam name="TDomainObject">
    /// The type for associated domain objects.
    /// </typeparam>
    /// <typeparam name="TRepository">
    /// The type for the repository storing domain objects.
    /// </typeparam>
    /// <remarks>
    /// This implementation of Unit of Work (<see cref="http://msdn.microsoft.com/en-us/magazine/dd882510.aspx"/>) is a combination of the unit itself and an underlying
    /// object repository.
    /// 
    /// That is work objects are stored in memory and committing means definitevly storing objects in the target repository (meaning the persistance object store). 
    /// </remarks>
    public abstract class UnitOfWork<TRepository, TDomainObjectIdentifierName, TDomainObject> : SinglePatternInstance
        where TDomainObjectIdentifierName : IEquatable<TDomainObjectIdentifierName>
        where TDomainObject : DomainObject<TDomainObjectIdentifierName>, ICloneable
        where TRepository : Repository<TDomainObjectIdentifierName, WorkObject<TDomainObjectIdentifierName, TDomainObject>>, IEnumerable<WorkObject<TDomainObjectIdentifierName, TDomainObject>>, new()
    {
        /// <summary>
        /// Default constructor.
        /// </summary>
        public UnitOfWork()
        {
            workStore = new TRepository();
        }

        private readonly TRepository workStore;

        /// <summary>
        /// Underlying work repository.
        /// </summary>
        protected TRepository WorkStore
        {
            get
            {
                return workStore;
            }
        }

        /// <summary>
        /// Gets a reference to the repository reponsible of permanently-storing domain objects.
        /// </summary>
        protected abstract Repository<TDomainObjectIdentifierName, TDomainObject> TargetRepository
        {
            get;
        }

        /// <summary>
        /// Gets an iterable of objects that must be created within the unit.
        /// </summary>
        protected IEnumerable<WorkObject<TDomainObjectIdentifierName, TDomainObject>> NewObjects
        {
            get
            {
                return WorkStore.Where<WorkObject<TDomainObjectIdentifierName, TDomainObject>>(someWorkObject => someWorkObject.New);
            }
        }

        /// <summary>
        /// Gets an iterable of objects that must be updated within the unit.
        /// </summary>
        protected IEnumerable<WorkObject<TDomainObjectIdentifierName, TDomainObject>> UpdatedObjects
        {
            get
            {
                return WorkStore.Where<WorkObject<TDomainObjectIdentifierName, TDomainObject>>(someWorkObject => !someWorkObject.Dirty);
            }
        }

        /// <summary>
        /// Gets an iterable of objects that must be deleted within the unit.
        /// </summary>
        protected IEnumerable<WorkObject<TDomainObjectIdentifierName, TDomainObject>> DeletedObjects
        {
            get
            {
                return WorkStore.Where<WorkObject<TDomainObjectIdentifierName, TDomainObject>>(someWorkObject => someWorkObject.Delete);
            }
        }

        /// <summary>
        /// Marks a domain object as new work.
        /// </summary>
        /// <param name="someObject">
        /// The new object.
        /// </param>
        public virtual void MarkNew(ref TDomainObject someObject)
        {
            ValidateTargetRepository();

            WorkStore.Add(new WorkObject<TDomainObjectIdentifierName, TDomainObject>(someObject, true));
        }

        /// <summary>
        /// Marks that a work is dirty.
        /// </summary>
        /// <param name="someObject">
        /// The associated object with some work.
        /// </param>
        public virtual void MarkDirty(ref TDomainObject someObject)
        {
            ValidateTargetRepository();

            WorkObject<TDomainObjectIdentifierName, TDomainObject> workObject = WorkStore.GetByID(someObject.ID);
            workObject.DomainObject.DateModified = workObject.DomainObject.DateAdded;

            WorkStore.Update(workObject);
        }

        /// <summary>
        /// Marks that a work and its associated domain object is going to be deleted.
        /// </summary>
        /// <param name="someObject">
        /// The object to be deleted.
        /// </param>
        public virtual void MarkDeleted(ref TDomainObject someObject)
        {
            ValidateTargetRepository();

            WorkObject<TDomainObjectIdentifierName, TDomainObject> work = WorkStore.GetByID(someObject.ID);
            work.Delete = true;

            WorkStore.Update(work);
        }


        /// <summary>
        /// Permantly stores works in their associated repositories.
        /// </summary>
        public virtual void Commit()
        {
            ValidateTargetRepository();

            foreach (WorkObject<TDomainObjectIdentifierName, TDomainObject> work in NewObjects)
            {
                try
                {
                    TargetRepository.Add(work.DomainObject);
                }
                catch(Exception e)
                {
                    throw new InvalidOperationException("Cannot commit a domain object to the target repository", e);
                }

                WorkStore.Remove(work);
            }

            foreach (WorkObject<TDomainObjectIdentifierName, TDomainObject> work in UpdatedObjects)
            {
                try
                {
                    TargetRepository.Update(work.DomainObject);
                }
                catch (Exception e)
                {
                    throw new InvalidOperationException("Cannot commit a domain object to the target repository", e);
                }

                WorkStore.Remove(work);
            }

            foreach (WorkObject<TDomainObjectIdentifierName, TDomainObject> workObject in DeletedObjects)
            {
                try
                {
                    TargetRepository.Remove(workObject.DomainObject);
                }
                catch (Exception e)
                {
                    throw new InvalidOperationException("Cannot commit a domain object to the target repository", e);
                }

                WorkStore.Remove(workObject);
            }
        }

        /// <summary>
        /// Retrieves initial values for the associated works.
        /// </summary>
        public virtual void Rollback()
        {
            ValidateTargetRepository();

            ICollection<WorkObject<TDomainObjectIdentifierName, TDomainObject>> dirtyWorkList = new List<WorkObject<TDomainObjectIdentifierName, TDomainObject>>();

            foreach (WorkObject<TDomainObjectIdentifierName, TDomainObject> work in WorkStore)
            {
                if (!work.New)
                {
                    dirtyWorkList.Add
                    (
                        new WorkObject<TDomainObjectIdentifierName, TDomainObject>
                        (
                            work.DirtyDomainObject
                        )
                    );
                }
            }

            foreach (WorkObject<TDomainObjectIdentifierName, TDomainObject> work in WorkStore)
            {
                WorkStore.Remove(work);
            }

            foreach (WorkObject<TDomainObjectIdentifierName, TDomainObject> dirtyWork in dirtyWorkList)
            {
                WorkStore.Add(dirtyWork);
            }
        }

        /// <summary>
        /// Releases related resources and roll-backs the work.
        /// </summary>
        public override void Dispose()
        {
            Rollback();
        }

        /// <summary>
        /// Not in use.
        /// </summary>
        public override void WarmUp()
        {
        }

        private void ValidateTargetRepository()
        {
            if (TargetRepository == null)
            {
                throw new NullReferenceException("Target repository cannot be null");
            }
        }
    }
}